Promise와 async/await

Promise와 async/await

JS setTimeout 같은 함수를 통해 비동기 처리를 할 수 있다.

비동기 함수가 실행 된 이후를 보장하기 위해 callback 을 사용한다.

function loadScript(src, callback){
    const script = document.createElement('script')
    script.src = src;
    script.onload = () => callback(null, script)
    script.onerror = () => callback(new Error(`Failed to load ${src}`))
    
    document.head.append(script)
}

위처럼 callback 함수로 loadScript 완료 이후의 실행을 보장할 수 있다.

하지만 그런 비동기 작업이 연속적으로 이어져야 하는 경우 다음과 같은 콜백 지옥을 만날 수 있다.

loadScript(srcA, () => {
    loadScript(srcB, () => {
        loadScript(srcC, () => {
            loadScript(srcD, () => {
                loadScript(srcE, () => {
                    // ...
                })
            })
        })
    })
})

위의 방법을 해결하는 여러 방법 중 한가지가 Promise 이다.

Promise

Promise 는 기본적으로 아래와 같이 사용한다.

const promise = new Promise((resolve, reject) => {
    // ...
})

/*
  promise : {
  	state: "pending",
  	result: undefined
  }
*/

위에서 Promise에 전달되는 함수를 실행자 함수 라고 표현하고, 인수는 resolvereject 를 함수로 받는다.

실행자 함수에서는 결과를 언제 얻든 무조건 두 인수 중 하나를 실행해야 한다.

성공적으로 실행된 경우 resolve(value) 를, 실패한 경우 reject(error) 를 실행하면 된다.

new Promise 를 실행하게 되면 주석 속 promise 객체를 받게 된다.

이는 resolvereject 실행 여부에 따라 값이 변한다.


//성공한 경우
const resolved = {
  state: "fullfilled", 
  result: value
}

//실패한 경우
const rejected = {
  state: "rejected",
  result: error
}

then, catch, finally

Promise에서 실행사 함수의 결과를 받는 방법으로는 then , catch , finally 같은 소비 함수를 필요로 한다.

then

resolve 가 호출되었을 때 호출된다.

인자는 최대 두 개 까지 받을 수 있으며 인자 순서는 성공 시 호출 함수, 실패 시 호출 함수 순이다.

catch

reject 가 호출되었을 때 단순히 에러만 받고 싶은 경우 호출된다.

.then(null, callback).catch(callback) 과 동일하게 동작한다.

finally

try {...} catch {...}finally 같은 역할을 한다.

.then(callback, callback).finally(callback) 과 비슷하게 동작하지만 차이점이 있다.

finally 에서는 성공 여부를 알 수 없으며, 다음 핸들러에 결과와 에러를 전달한다.

new Promise((resolve) => {
    setTimeout(() => resolve('done'), 1000)
})
  .finally(() => console.log('finally'))
  .then(console.log)

/*
finally
done

위 처럼 출력되며 'done' 이 finally에서 then으로 전달된다.
*/

Promise chaining

Promise에서는 then 에서 thenable 을 반환하기 때문 then 을 계속 이어 사용할 수 있다.

return 을 주게 되면 다음 then 에서는 인자도 받을 수 있다.

return 에는 값이나 새로운 Promise가 올 수 있다.

이 때 모든 Promise는 thenable 이지만 모든 thenable 이 Promise는 아니다.

// 0, 1, 2 를 출력한다.
new Promise((res) => {
  let e = 0
  console.log(e++)
  res(e)
}).then((e ) => {
  console.log(e++)
  return e
}).then(e => {
  console.log(e++)
  return e
})

// 0, 1, 2 를 1초 간격으로 호출한다.
new Promise((resolve) => {
  console.log(0)
  setTimeout(() => resolve(1), 1000);
}).then((e) => {
  console.log(e)
  return new Promise((resolve) => {
    setTimeout(() => resolve(e+1), 1000);
  })
}).then((e) => {
  console.log(e)
  return new Promise((resolve) => {
    setTimeout(() => resolve(e+1), 1000);
  })
})

Promise의 에러 핸들링

Promise 에서 에러를 핸들링 하기 위해 catch를 사용한다.

catch 는 항상 첫 핸들러일 필요가 없으며, 여러 개then 뒤에 올 수 있다.

여러 then 체인 뒤에 catch 가 있다면, 하나라도 then이 실패했을 때 catch 는 호출된다.

function someFunction(){} // new Promise를 리턴하는 함수

// 아래에서는 someFunction1-3 중에 하나라도 오류가 나면 catch의 catchFunction이 호출된다.
someFunction()
  .then(someFunction1())
  .then(someFunction1())
  .then(someFunction1())
  .catch(catchFunction())

암시적 try/catch

Promise의 실행자와 핸들러 코드에는 암시적 try/catch 가 있다.

// 아래 두 코드는 동일하게 동작한다.
new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(console.log); // Error: 에러 발생!

new Promise((resolve, reject) => {
  reject(new Error("에러 발생!"));
}).catch(console.log); // Error: 에러 발생!

암시적 try/catch 는 명시적인 오류 뿐만 아니라, 핸들러 위에서 발생한 다른 오류도 잡는다.

new Promise((resolve, reject) => {
  resolve("OK");
}).then((result) => {
  blabla(); // 존재하지 않는 함수
}).catch(console.log); // ReferenceError: blabla is not defined

다시 던지기

catch 에 의해서 오류를 다시 수정하고, 그 결과에 따라 다시 then 이나 catch 로 받을 수 있습니다.

const rethrowTest = (fixable) => new Promise((resolve, reject) => reject(fixable))
  // 오류가 여기서 한번 처리된다.
  .catch(e => {
    if(e) {
      return 'resolved'
    }
    throw new Error('rejected')
  }) // 수정 여부에 따라 return 이 되면 then이, throw가 되면 catch가 실행된다.
  .then(console.log)
  .catch(console.log)

rethrowTest(true) // resolved
rethrowTest(false) // Error: rejected...

처리되지 못한 오류

만약 catch 또는 then 의 두 번째 인자로 오류를 처리하지 않는 경우 Promise는 전역 오류를 생성한다.

그 자리에서 catch 를 넣어 잡는 것이 가장 최선이겠지만 아래 방법으로 처리할 수 있다.

window.addEventListener('unhandledrejection', function(event) {
  console.log(event.promise); // 에러가 생성된 Promise
  console.log(event.reason); // 처리되지 못한 오류
});

// 핸들러가 없는 오류 발생
new Promise(function() {
  throw new Error("에러 발생!");
}); 

Written by@[esllo]
plain developer

GitHubTwitterLinkedIn